package com.griddroid.serverdaemons.threadhandlers;

import java.io.Closeable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import com.griddroid.configuration.GridConfig;
import com.griddroid.messages.EndSessionAckMessage;
import com.griddroid.messages.EndSessionRequestMessage;
import com.griddroid.messages.PingAckMessage;
import com.griddroid.messages.PingMessage;
import com.griddroid.messages.StartSessionAckMessage;
import com.griddroid.nodes.GridNode;
import com.griddroid.nodes.GridServer;

/**
 * A generic class for handling a node
 * 
 * 
 * @author Charalampos Mavroforakis
 * @author Thomas Ailianos
 * @author Michail Michailidis
 * 
 */
public abstract class GenericHandler implements Runnable, Closeable
{
	private String id;
	/**
	 * the socket that will wait for the connection to be established
	 */
	protected ServerSocket handlerSocket;
	/**
	 * the server that this handlers belongs to
	 */
	protected GridServer server;
	/**
	 * the socket through which the node tries to connect to the server
	 */
	protected Socket firstSocket;
	/**
	 * the established and permanent connection between the node and the server
	 */
	protected Socket permanentSocket;
	/**
	 * time when the previous ping happened
	 */
	protected long lastPing;
	/**
	 * used to check if it is the right moment to send a ping message
	 */
	protected int checkCounter;
	/**
	 * if set to <code>true</code>, the handler exits its endless loop
	 */
	protected boolean forceExit = false;
	/**
	 * the port that can be used to establish a permanent connection
	 */
	protected int currentPort;

	@Override
	public void run()
	{
	}

	@Override
	public void close()
	{
	}

	/**
	 * 
	 * @return the ID of the handler
	 */
	public String getId()
	{
		return id;
	}

	public void setId(String id)
	{
		this.id = id;
	}

	/**
	 * gets the temporary connection and establishes a new, permanent one. It
	 * tries to reserver an open port and sends that port to the node through a
	 * {@link StartSessionAckMessage StartSessionAckMessage} At the same time it
	 * builds a ServerSocket on that port, waiting for the node to connect.
	 * 
	 * @see ServerSocket
	 * @see StartSessionAckMessage
	 * @see GridNode#connectToServer()
	 */
	protected void acceptConnection()
	{
		// SEND ACKNOWLEDGEMENT MESSAGE
		try
		{
			System.out.println(this.server.printPorts());
			int port = this.server.getFreePort();
			System.out.println(port);
			handlerSocket = new ServerSocket(port);
			handlerSocket.setReuseAddress(true);

		}
		catch (IOException e1)
		{
			this.server.log("Error", "Could not initialize handler:Could not send acknowledgement message." + e1.getMessage());
			return;
		}

		try
		{
			currentPort = handlerSocket.getLocalPort();
			StartSessionAckMessage message = new StartSessionAckMessage(handlerSocket.getLocalPort(), this.getId());
			this.send(this.firstSocket, message);
			this.firstSocket.close();
			this.firstSocket = null;
		}
		catch (IOException e1)
		{
			this.server.log("Error", "STEP2:Could not initialize handler:Could not send acknowledgement message." + e1.getMessage());
			return;
		}

		this.server.log("Info", "Sended connection acknowledgement");

		this.server.log("Info", "Waiting node permanent connection");
		try
		{
			this.permanentSocket = handlerSocket.accept();

			this.server.log("Info", "Node connected with the handler with id " + this.getId() + ".");
		}
		catch (IOException e)
		{
			this.server.log("Error", "Could not bind to a new port to accept connection.");
		}
		this.lastPing = System.currentTimeMillis() / 1000;
	}

	/**
	 * waits and receives an object from a socket
	 * 
	 * @param socket the socket which the handler listens to
	 * @return the object that was sent
	 * @throws IOException problem with the socket or the input stream
	 * @throws ClassNotFoundException
	 */
	protected Object receive(Socket socket) throws IOException, ClassNotFoundException
	{
		return (new ObjectInputStream(socket.getInputStream())).readObject();
	}

	/**
	 * sends an object over a socket
	 * 
	 * @param socket the socket that will be used for the transfer
	 * @param obj the object that will be transmitted
	 * @throws IOException problem with the socket or the output stream
	 */
	protected void send(Socket socket, Object obj) throws IOException
	{
		(new ObjectOutputStream(socket.getOutputStream())).writeObject(obj);
	}

	/**
	 * check the ping message and then reset the ping time
	 * 
	 * @param obj the message to check
	 */
	protected void acceptPingAckMessage(Object obj)
	{
		if (((PingAckMessage) obj).getNodeId().equals(this.getId()))
		{
			this.lastPing = System.currentTimeMillis() / 1000;
			this.server.log("Info", "Received ping ack message from node with id " + this.getId());
		}
		else
		{
			this.server.log("Error", "Hanlder with id " + this.getId() + " received unknown ping ack message");
		}
	}

	/**
	 * if the message is an End Session, then the handler sends back the acknowledgement
	 * @param obj the message to check
	 * @throws IOException
	 */
	protected void acceptEndSessionMessage(Object obj) throws IOException
	{
		if (((EndSessionRequestMessage) obj).getId().equals(this.getId()))
		{
			this.server.log("Info", "Received end session message from node with id " + this.getId());
			EndSessionAckMessage mess = new EndSessionAckMessage(this.getId());
			this.send(this.permanentSocket, mess);
			// this.close();
		}
		else
		{
			this.server.log("Error", "Hanlder with id " + this.getId() + " received unknown session end message");
		}
	}

	protected void sendPings()
	{
		if (checkCounter * GridConfig.CHECKINTERVAL > GridConfig.PINGINTERVAL)
		{
			this.server.log("Info", "Sending ping to node with id " + this.getId());
			PingMessage ping = new PingMessage(this.getId());
			try
			{
				this.send(this.permanentSocket, ping);
			}
			catch (IOException e1)
			{
				this.server.log("Error", "Hanlder with id " + this.getId() + " could not send ping to the node");
			}
			checkCounter = 0;
		}
	}

	/**
	 * if a node has not replied for too long, kill the handler
	 * @return true if the node has replied
	 * 
	 */
	protected boolean checkPingsTimeouts()
	{
		if (System.currentTimeMillis() / 1000 - this.lastPing > GridConfig.ALIVETIMEOUT)
		{
			this.close();
			return false;
		}
		return true;
	}
}
